package fr.vvins.core.common.dto;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import fr.vvins.core.dao.Entity;

public class EntityDtoConverter {
	

	private Map<String, List<DtoProperty>> introspectorCache;
	private DtoFactory dtoFactory;
	
    public EntityDtoConverter() {
    	introspectorCache = Collections.synchronizedMap(new HashMap<String, List<DtoProperty>>());
	}
    
    /**
	 * This method return an Dto. The reall type is link to the mapping 
	 * define in map itemDescriptorMapping. All property of Dto is filled by
	 * the corresponding property of curItem Entity Item .
	 * 
	 * @param curItem : the source Entity item
	 * @return The valued Dto.
	 * 
	 * @throws DtoConvertException
	 */
    public BeanDto convertEntityToDto (Entity curItem) throws DtoException {
		return convertEntityToDto(curItem, new ArrayList<String>());
	}
    
    /**
	 * This method return an DtoInfo. The reall type is link to the mapping 
	 * define in map itemDescriptorMapping. All property of Dto is filled by
	 * the corresponding property of curItem Entity Item .
	 * 
	 * @param curItem : the source Entity item
	 * @return The valued Dto.
     * @throws DtoException 
	 * 
	 * @throws DtoConvertException
	 */
	public BeanDtoInfo convertEntityToDtoInfo(Entity curItem) throws DtoException {
		return convertEntityToDtoInfo(curItem,  new ArrayList<String>());
	}
	
    /**
	 * This method return an DtoInfo. The reall type is link to the mapping 
	 * define in map itemDescriptorMapping. All property of Dto is filled by
	 * the corresponding property of curItem Entity Item except excludedProperties of
	 * Dto.
	 * 
	 * @param curItem : the source Entity item
	 * @param excludedProperties : properties list of Dto
	 * @return The valued Dto.
     * @throws DtoException 
	 * 
	 * @throws DtoConvertException
	 */
	public BeanDtoInfo convertEntityToDtoInfo(Entity curItem, List<String> excludedProperties) throws DtoException {
		BeanDtoInfo dto = dtoFactory.createDtoInfo(curItem.getClass());
		return convertEntityToDto(curItem,dto,excludedProperties);
	}
	
    /**
	 * This method return an Dto. The reall type is link to the mapping 
	 * define in map itemDescriptorMapping. All property of Dto is filled by
	 * the corresponding property of curItem Entity Item except excludedProperties of
	 * Dto.
	 * 
	 * @param curItem : the source Entity item
	 * @param excludedProperties : properties list of Dto
	 * @return The valued Dto.
	 * 
	 * @throws DtoConvertException
	 */
	public BeanDto convertEntityToDto(Entity curItem, List<String> excludedProperties) throws DtoException {
		BeanDto dto = dtoFactory.createDto(curItem.getClass());
		return (BeanDto)convertEntityToDto(curItem,dto,excludedProperties);
	}
	
	/**
	 * This method return an Dto. All property of Dto is filled by
	 * the corresponding property of curItem Entity Item.
	 * 
	 * @param curItem : the source Entity item
	 * @param target : the target Dto
	 * @return The valued Dto.
	 * 
	 * @throws DtoConvertException
	 */
	public BeanDtoInfo convertEntityToDto (Entity curItem, BeanDtoInfo target) throws DtoException {
		return convertEntityToDto(curItem, target, null);
	}
	
	/**
	 * This method return an Dto. All property of Dto is filled by
	 * the corresponding property of curItem Entity Item except excludedProperties of
	 * Dto.
	 * 
	 * @param curItem : the source Entity item
	 * @param target : the target Dto
	 * @param excludedProperties : properties list of Dto
	 * @return The valued Dto.
	 * 
	 * @throws DtoConvertException
	 */
	public BeanDtoInfo convertEntityToDto (Entity curItem, BeanDtoInfo target, List<String> excludedProperties) throws DtoException {

		setStandardItemProperties(curItem, target);
		List<DtoProperty> properties = new ArrayList<DtoProperty>();
		
		if(getIntrospectorCache().containsKey(target.getClass().getName())){
			properties = introspectorCache.get(target.getClass().getName());
		} else {
			List<DtoProperty> targetProperties = DtoUtils.getFields(target);
			for(DtoProperty property : targetProperties){
				if(DtoUtils.hasProperty(curItem, property.getName())){
					properties.add(property);
				}
			}
			getIntrospectorCache().put(target.getClass().getName(), properties);
		}
		for(DtoProperty property : properties){
			if(excludedProperties==null || !excludedProperties.contains(property.getName())){
				DtoUtils.setPropertyValue(target, property.getName(), getPropertyValue(curItem,property));
			}
		}
		return target;
	}
	
	
	/**
	 * This method allow to analyze a specific property of the curItem.
	 * 
	 * @param curItem : target Entity item
	 * @param property : the property to analyze
	 * @return an Object, could be Set, List, Array, Dto or a simple scalar object.
	 * 
	 * @throws IntrospectionException
	 * @throws DtoConvertException
	 * @throws EntityException
	 */
	public Object getPropertyValue(Entity curItem, DtoProperty property) throws DtoException {
		
		if(!DtoUtils.hasProperty(curItem, property.getName())){
			return null;
		} else if(DtoUtils.getPropertyValue(curItem, property.getName()) == null){
			return null;
		} else if(property.isCollection()){
			Collection collection = (Collection) DtoUtils.getPropertyValue(curItem, property.getName());
			return (!collection.isEmpty())? convertEntityCollectionToDto(collection, property):null;
		} else if (property.isArray()) {
			Object[] array = (Object[]) DtoUtils.getPropertyValue(curItem, property.getName());
			return (array.length > 0)? convertEntityArrayToDto(array, property):null;
		} else if(property.isMap()){
			Map map = (Map)DtoUtils.getPropertyValue(curItem, property.getName());
			return (!map.isEmpty())?convertEntityMapToDto(map,property):null;
		}  else if(BeanDtoInfo.class.isAssignableFrom(property.getType())){
			try {
				return convertEntityToDto((Entity)DtoUtils.getPropertyValue(curItem, property.getName()), (BeanDtoInfo)property.getType().newInstance());
			} catch (InstantiationException e) {
				throw new DtoConvertException(e.getMessage(),e);
			} catch (IllegalAccessException e) {
				throw new DtoConvertException(e.getMessage(),e);
			}
		} else {
			return DtoUtils.getPropertyValue(curItem, property.getName());
		}
	}

	/**
	 * This method allow to analyze a specific property of the curItem.
	 * 
	 * @param curItem : target Entity item
	 * @param property : the property to analyze
	 * @return an Object, could be Set, List, Array, Dto or a simple scalar object.
	 * 
	 * @throws IntrospectionException
	 * @throws DtoConvertException
	 * @throws EntityException
	 */
	public Object convertValue(Object value, DtoProperty property) throws DtoException {
		
		if(property.isCollection()){
			Collection collection = (Collection) value;
			return (!collection.isEmpty())? convertEntityCollectionToDto(collection, property):null;
		} else if (property.isArray()) {
			Object[] array = (Object[]) value;
			return (array.length > 0)? convertEntityArrayToDto(array, property):null;
		} else if(property.isMap()){
			Map map = (Map)value;
			return (!map.isEmpty())?convertEntityMapToDto(map,property):null;
		}  else if(BeanDtoInfo.class.isAssignableFrom(property.getType())){
			try {
				return convertEntityToDto((Entity)value, (BeanDtoInfo)property.getType().newInstance());
			} catch (InstantiationException e) {
				throw new DtoConvertException(e.getMessage(),e);
			} catch (IllegalAccessException e) {
				throw new DtoConvertException(e.getMessage(),e);
			}
		} else {
			return value;
		}
	}

	
	/**
	 * Convert Entity array a array of Dto
	 * @param propertyArray
	 * @param property
	 * @return
	 * @throws DtoConvertException
	 */
	private Object convertEntityArrayToDto(Object[] propertyArray, DtoProperty property) throws DtoException {
		if(propertyArray == null){
			return null;
		}		
		List valuesList = new ArrayList();
		int i = 0;
		for(Object propertyValue : propertyArray){
			if(propertyValue!= null && property.getDataTypes() != null){
				valuesList.add(convertValue(propertyValue, property.getDataTypes()[0]));
			} else {
				valuesList.add(propertyValue);
			}
			i++;
		}
		Object[] values = (Object[]) Array.newInstance(property.getType(), valuesList.size());
		valuesList.toArray(values);
		return values;
	}

	
	/**
	 * Convert Entity collection a Collection of Dto
	 * @param propertyValues
	 * @param property
	 * @return
	 * @throws DtoConvertException
	 */
	private Object convertEntityCollectionToDto(Collection propertyValues, DtoProperty property) throws DtoException {
		if(propertyValues == null){
			return null;
		}	
		Collection values = null;
		if(Set.class.isAssignableFrom(property.getType())){
			values = new HashSet();
		} else if (List.class.isAssignableFrom(property.getType())){
			values = new ArrayList();
		}
		for(Object propertyValue : propertyValues){
			if(propertyValue!= null && property.getDataTypes() != null ){
				values.add(convertValue(propertyValue, property.getDataTypes()[0]));
			} else {
				values.add(propertyValue);
			}
		}
		return values;
	}
	


	/**
	 * Convert Entity Map to AtgMap
	 * @param pItemMap
	 * @return
	 * @throws EntityException
	 * @throws DtoConvertException 
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 */
	protected Object convertEntityMapToDto(Map pEntityMap, DtoProperty property) throws DtoException {
		if(pEntityMap == null){
			return null;
		}
		Map propertyMap = new HashMap();
		for(Object key : pEntityMap.keySet()) {
			Object value = pEntityMap.get(key);
			propertyMap.put(convertValue(key, property.getDataTypes()[0]), convertValue(value, property.getDataTypes()[1]));
				
		}
		return propertyMap;
	}
	
	/**
	 * Set standard Dto properties to AtgAssetInfos instance
	 * @param pItem
	 * @param pDto
	 * @throws EntityException
	 */
	protected void setStandardItemProperties(Entity pItem, BeanDtoInfo pDto) throws DtoConvertException{
		pDto.setId(pItem.getId());
	}

	
	public Map<String, List<DtoProperty>> getIntrospectorCache() {
		return introspectorCache;
	}

	public void setIntrospectorCache(Map<String, List<DtoProperty>> introspectorCache) {
		this.introspectorCache = introspectorCache;
	}

	public DtoFactory getDtoFactory() {
		return dtoFactory;
	}

	public void setDtoFactory(DtoFactory dtoFactory) {
		this.dtoFactory = dtoFactory;
	}

	
}
